/**
 *  File: main.c
 *  Time: 2022年1月13日
 *  Author: 荣涛
 *
 *  统计 dnf list 格式文件，从metadata2文件夹中读取元数据，元数据通过
 *  使用 dnf list 命令或其他可生成这个格式文件的命令生成。
 */
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <stdbool.h>
#include <malloc.h>
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
#include <string.h>
#define NDEBUG
#include <assert.h>
#define RB_COMPACT // (Optional, embed color bits in right-child pointers.)
#include "list.h"
#include "rb.h"

#define NAME_noarch     "noarch"
#define NAME_i686       "i686"
#define NAME_x86_64     "x86_64"
#define NAME_aarch64    "aarch64"
#define NAME_ppc64le    "ppc64le"
#define NAME_s390x      "s390x"

#define OSNAME_CENTOS_8_4       "8.4"
#define OSNAME_CENTOS_8_5       "8.5"
#define OSNAME_CENTOS_8_STREAM  "8-stream"
#define OSNAME_CENTOS_9_STREAM  "9-stream"
#define OSNAME_FEDORA_35        "F35"

#if 0
enum {
    RPM_BUILDHOST,
    RPM_BUILDTIME,
    RPM_DESCRIPTION,
    RPM_EPOCH,
    RPM_INSTALLTIME,
    RPM_NAME,
    RPM_RELEASE,
    RPM_SIZE,
    RPM_SUMMARY,
    RPM_VERSION,
};
#endif

typedef enum {
    ARCH_noarch,
    ARCH_i686,
    ARCH_x86_64,
    ARCH_aarch64,
    ARCH_ppc64le,
    ARCH_s390x,
    ARCH_NUM,
} arch_type;

const char *arch2string[] = {
    [ARCH_noarch] = NAME_noarch,
    [ARCH_i686] = NAME_i686,
    [ARCH_x86_64] = NAME_x86_64,
    [ARCH_aarch64] = NAME_aarch64,
    [ARCH_ppc64le] = NAME_ppc64le,
    [ARCH_s390x] = NAME_s390x,
};

typedef enum {
    OS_CENTOS_8_4,
    OS_CENTOS_8_5,
    OS_CENTOS_8_STREAM,
    OS_CENTOS_9_STREAM,
    OS_FEDORA_35,
} os_type;

const char *os2string[] = {
    [OS_CENTOS_8_4] = OSNAME_CENTOS_8_4,
    [OS_CENTOS_8_5] = OSNAME_CENTOS_8_5,
    [OS_CENTOS_8_STREAM] = OSNAME_CENTOS_8_STREAM,
    [OS_CENTOS_9_STREAM] = OSNAME_CENTOS_9_STREAM,
    [OS_FEDORA_35] = OSNAME_FEDORA_35,
};

struct repo;

struct pkg {
    char *name;
    char *evr;
    arch_type arch;
    struct repo *repo;
    rb_node(struct pkg) repo_node; // root is struct repo.pkgs
    struct list all_pkgs_node; // root in struct pkgs_node.pkgs_list
};

/**
 *  Repo 中的 pkgs tree
 */
typedef rb_tree(struct pkg) repo_pkg_tree_t;
rb_proto(static, repo_pkg_tree_, repo_pkg_tree_t, struct pkg);
static int repo_pkg_tree_cmp(const struct pkg *a_node, const struct pkg *a_other)
{
    return strcmp(a_node->name, a_other->name);
}
rb_gen(static, repo_pkg_tree_, repo_pkg_tree_t, struct pkg, repo_node, repo_pkg_tree_cmp);

/**
 *  所有 pkgs 都将保存到这个 tree
 */
struct pkgs_node {
    char *name;
    rb_node(struct pkgs_node) same_name_node;
    unsigned int npkg;
    struct list pkgs_list; // node in struct pkg.all_pkgs_node
};
typedef rb_tree(struct pkgs_node) all_pkgs_tree_t;
rb_proto(static, all_pkgs_tree_, all_pkgs_tree_t, struct pkgs_node);
static int pkgs_tree_cmp(const struct pkgs_node *a_node, const struct pkgs_node *a_other)
{
    return strcmp(a_node->name, a_other->name);
}
rb_gen(static, all_pkgs_tree_, all_pkgs_tree_t, struct pkgs_node, same_name_node, pkgs_tree_cmp);

struct repo {
    char *name;
    os_type os;
    int arch;
    unsigned int npkg;
    unsigned int arch_npkgs[ARCH_NUM];
    repo_pkg_tree_t pkgs; // node is struct pkg.repo_node
    struct list list;   // root is `repolist`
};

struct repo_operations {
    void (*pre_handler)(struct repo *repo);
    void (*handler)(struct repo *repo, void *arg);
    void *handler_arg;
    void (*post_handler)(struct repo *repo);
};

static struct list repolist;
static all_pkgs_tree_t all_pkgs;

int parse_repolist_file(char *filename, os_type os, arch_type arch);

static const char* good_repo_name(char *reponame)
{
    if (strcasestr(reponame, "appstream")) {
        if (strcasestr(reponame, "source")) return "AppStream.src";
        else return "AppStream";
    }
    else if (strcasestr(reponame, "baseos")) {
        if (strcasestr(reponame, "source")) return "BaseOS.src";
        else return "BaseOS";
    }
    else if (strcasestr(reponame, "devel")) {
        if (strcasestr(reponame, "source")) return "Devel.src";
        else return "Devel";
    }
    else if (strcasestr(reponame, "HighAvailability")) {
        if (strcasestr(reponame, "source")) return "HighAvailability.src";
        else return "HighAvailability";
    }
    else if (strcasestr(reponame, "PowerTools")) {
        if (strcasestr(reponame, "source")) return "PowerTools.src";
        else return "PowerTools";
    }
    else if (strcasestr(reponame, "crb")) {
        if (strcasestr(reponame, "source")) return "CRB.src";
        else return "CRB";
    }
    else if (strcasestr(reponame, "nfv")) {
        if (strcasestr(reponame, "source")) return "NFV.src";
        else return "NFV";
    }
    else if (strcasestr(reponame, "rt")) {
        if (strcasestr(reponame, "source")) return "RT.src";
        else return "RT";
    }
    else if (strcasestr(reponame, "ResilientStorage")) {
        if (strcasestr(reponame, "source")) return "ResilientStorage.src";
        else return "ResilientStorage";
    }
    else if (strcasestr(reponame, "fedora")) {
        if (strcasestr(reponame, "server")) return "Server";
        else if (strcasestr(reponame, "everything")) return "Everything";
    }
    else return "Unknown";
}

void pre_print_repo(struct repo *repo)
{
    printf("%-10s %-10s %-20s %-20s %12s %20s %-8s\n", 
            "", "", "", "<------------------", "xxx.ARCH.rpm", "------------------>", "");
    printf("\033[1;7m%-10s %-10s %-20s %-8s %-8s %-8s %-8s %-8s %-8s %-8s\033[m\n", 
            "OS", "Arch", "REPO", "noarch", "x86_64", "aarch64", "i686", "ppc64le", "s390x", "Total");
}

void print_repo(struct repo *repo, void *arg)
{
    printf("%-10s %-10s %-20s %-8d %-8d %-8d %-8d %-8d %-8d %-8d\n", 
        os2string[repo->os],
        repo->arch>=0?arch2string[repo->arch]:"src",
        good_repo_name(repo->name), 
        repo->arch_npkgs[ARCH_noarch],
        repo->arch_npkgs[ARCH_x86_64],
        repo->arch_npkgs[ARCH_aarch64],
        repo->arch_npkgs[ARCH_i686],
        repo->arch_npkgs[ARCH_ppc64le],
        repo->arch_npkgs[ARCH_s390x],
        repo->npkg);
}

void post_print_repo(struct repo *repo)
{
    printf("\n");
}

struct repo_operations repo_print_operation = {
    .pre_handler = pre_print_repo,
    .handler = print_repo,
    .handler_arg = NULL,
    .post_handler = post_print_repo,
};

static int vars_init()
{
    list_init(&repolist);
    all_pkgs_tree_new(&all_pkgs);
}

static int load_metadata()
{
#if 0
    parse_repolist_file("metadata/centos8_4_appstream", OS_CENTOS_8_4, ARCH_x86_64);
    parse_repolist_file("metadata/centos8_4_baseos", OS_CENTOS_8_4, ARCH_x86_64);
    parse_repolist_file("metadata/centos8_4_devel", OS_CENTOS_8_4, ARCH_x86_64);
    parse_repolist_file("metadata/centos8_4_highavailability", OS_CENTOS_8_4, ARCH_x86_64);
    parse_repolist_file("metadata/centos8_4_powertools", OS_CENTOS_8_4, ARCH_x86_64);

    parse_repolist_file("metadata/centos8_5_appstream", OS_CENTOS_8_5, ARCH_x86_64);
    parse_repolist_file("metadata/centos8_5_baseos", OS_CENTOS_8_5, ARCH_x86_64);
    parse_repolist_file("metadata/centos8_5_devel", OS_CENTOS_8_5, ARCH_x86_64);
    parse_repolist_file("metadata/centos8_5_highavailability", OS_CENTOS_8_5, ARCH_x86_64);
    parse_repolist_file("metadata/centos8_5_powertools", OS_CENTOS_8_5, ARCH_x86_64);
    
    parse_repolist_file("metadata/fedora_35_server", OS_FEDORA_35, ARCH_x86_64);

    parse_repolist_file("metadata/centos8_stream_appstream", OS_CENTOS_8_STREAM, ARCH_x86_64);
    parse_repolist_file("metadata/centos8_stream_baseos", OS_CENTOS_8_STREAM, ARCH_x86_64);
    parse_repolist_file("metadata/centos8_stream_devel", OS_CENTOS_8_STREAM, ARCH_x86_64);
    parse_repolist_file("metadata/centos8_stream_highavailability", OS_CENTOS_8_STREAM, ARCH_x86_64);
    parse_repolist_file("metadata/centos8_stream_powertools", OS_CENTOS_8_STREAM);

    parse_repolist_file("metadata/centos9_stream_appstream", OS_CENTOS_9_STREAM, ARCH_x86_64);
    parse_repolist_file("metadata/centos9_stream_baseos", OS_CENTOS_9_STREAM, ARCH_x86_64);
    parse_repolist_file("metadata/centos9_stream_highavailability", OS_CENTOS_9_STREAM, ARCH_x86_64);
    parse_repolist_file("metadata/centos9_stream_CRB", OS_CENTOS_9_STREAM, ARCH_x86_64);
    parse_repolist_file("metadata/centos9_stream_nfv", OS_CENTOS_9_STREAM, ARCH_x86_64);
    parse_repolist_file("metadata/centos9_stream_rt", OS_CENTOS_9_STREAM, ARCH_x86_64);
    parse_repolist_file("metadata/centos9_stream_resilientstorage", OS_CENTOS_9_STREAM, ARCH_x86_64);
#else
    parse_repolist_file("metadata2/centos_9_stream_aarch64_appstream", OS_CENTOS_9_STREAM, ARCH_aarch64);
    parse_repolist_file("metadata2/centos_9_stream_aarch64_baseos", OS_CENTOS_9_STREAM, ARCH_aarch64);
    parse_repolist_file("metadata2/centos_9_stream_aarch64_crb", OS_CENTOS_9_STREAM, ARCH_aarch64);
    parse_repolist_file("metadata2/centos_9_stream_aarch64_highavailability", OS_CENTOS_9_STREAM, ARCH_aarch64);

    parse_repolist_file("metadata2/centos_9_stream_source_appstream", OS_CENTOS_9_STREAM, -1);
    parse_repolist_file("metadata2/centos_9_stream_source_baseos", OS_CENTOS_9_STREAM, -1);
    parse_repolist_file("metadata2/centos_9_stream_source_crb", OS_CENTOS_9_STREAM, -1);
    parse_repolist_file("metadata2/centos_9_stream_source_highavailability", OS_CENTOS_9_STREAM, -1);
    parse_repolist_file("metadata2/centos_9_stream_source_resilientstorage", OS_CENTOS_9_STREAM, -1);
    parse_repolist_file("metadata2/centos_9_stream_source_rt", OS_CENTOS_9_STREAM, -1);

    parse_repolist_file("metadata2/centos_9_stream_x86_64_appstream", OS_CENTOS_9_STREAM, ARCH_x86_64);
    parse_repolist_file("metadata2/centos_9_stream_x86_64_baseos", OS_CENTOS_9_STREAM, ARCH_x86_64);
    parse_repolist_file("metadata2/centos_9_stream_x86_64_crb", OS_CENTOS_9_STREAM, ARCH_x86_64);
    parse_repolist_file("metadata2/centos_9_stream_x86_64_highavailability", OS_CENTOS_9_STREAM, ARCH_x86_64);
    parse_repolist_file("metadata2/centos_9_stream_x86_64_nfv", OS_CENTOS_9_STREAM, ARCH_x86_64);
    parse_repolist_file("metadata2/centos_9_stream_x86_64_resilientstorage", OS_CENTOS_9_STREAM, ARCH_x86_64);
    parse_repolist_file("metadata2/centos_9_stream_x86_64_rt", OS_CENTOS_9_STREAM, ARCH_x86_64);

    parse_repolist_file("metadata2/centos_8_5_aarch64_appstream", OS_CENTOS_8_5, ARCH_aarch64);
    parse_repolist_file("metadata2/centos_8_5_aarch64_baseos", OS_CENTOS_8_5, ARCH_aarch64);
    parse_repolist_file("metadata2/centos_8_5_aarch64_highavailability", OS_CENTOS_8_5, ARCH_aarch64);
    parse_repolist_file("metadata2/centos_8_5_aarch64_powertools", OS_CENTOS_8_5, ARCH_aarch64);

    parse_repolist_file("metadata2/centos_8_5_source_appstream", OS_CENTOS_8_5, -1);
    parse_repolist_file("metadata2/centos_8_5_source_baseos", OS_CENTOS_8_5, -1);
    parse_repolist_file("metadata2/centos_8_5_source_highavailability", OS_CENTOS_8_5, -1);
    parse_repolist_file("metadata2/centos_8_5_source_powertools", OS_CENTOS_8_5, -1);

    parse_repolist_file("metadata2/centos_8_5_x86_64_appstream", OS_CENTOS_8_5, ARCH_x86_64);
    parse_repolist_file("metadata2/centos_8_5_x86_64_baseos", OS_CENTOS_8_5, ARCH_x86_64);
    parse_repolist_file("metadata2/centos_8_5_x86_64_highavailability", OS_CENTOS_8_5, ARCH_x86_64);
    parse_repolist_file("metadata2/centos_8_5_x86_64_powertools", OS_CENTOS_8_5, ARCH_x86_64);

    parse_repolist_file("metadata2/centos_8_4_aarch64_appstream", OS_CENTOS_8_4, ARCH_aarch64);
    parse_repolist_file("metadata2/centos_8_4_aarch64_baseos", OS_CENTOS_8_4, ARCH_aarch64);
    parse_repolist_file("metadata2/centos_8_4_aarch64_highavailability", OS_CENTOS_8_4, ARCH_aarch64);
    parse_repolist_file("metadata2/centos_8_4_aarch64_powertools", OS_CENTOS_8_4, ARCH_aarch64);
    parse_repolist_file("metadata2/centos_8_4_x86_64_appstream", OS_CENTOS_8_4, ARCH_x86_64);
    parse_repolist_file("metadata2/centos_8_4_x86_64_baseos", OS_CENTOS_8_4, ARCH_x86_64);
    parse_repolist_file("metadata2/centos_8_4_x86_64_highavailability", OS_CENTOS_8_4, ARCH_x86_64);
    parse_repolist_file("metadata2/centos_8_4_x86_64_powertools", OS_CENTOS_8_4, ARCH_x86_64);
#endif
}

static void for_each_repo(struct repo_operations *ops)
{
    struct repo *iter_repo = NULL;
    if (ops->pre_handler)
        ops->pre_handler(NULL);

    list_for_each_entry(iter_repo, &repolist, list) {
        if (ops->handler)
            ops->handler(iter_repo, ops->handler_arg);
    }
    if (ops->post_handler)
        ops->post_handler(NULL);
}

static void repo_for_each_pkg(struct repo *repo)
{
    struct pkg *iter_pkg = NULL;
    for(iter_pkg = repo_pkg_tree_first(&repo->pkgs); 
        iter_pkg; 
        iter_pkg = repo_pkg_tree_next(&repo->pkgs, iter_pkg)) {
        printf("%s\n", iter_pkg->name);
    }
}

static void pkgs_for_each_pkg(const char *filter_name)
{
    const static char *colors[] = {
        "\033[1;31m",
        "\033[1;32m",
        "\033[1;33m",
        "\033[1;34m",
        "\033[1;35m",
    };
    const static int ncolors = 5;
    
    if (!filter_name) 
        return;

    int idx_color = 0;
    int print_npkgs = 0;
    struct pkgs_node *iter_pkg_node = NULL;

    printf("\033[1;7m%-32s %-32s %-16s %-12s %12s\033[m\n",
        "PKG.ARCH", "EVR", "REPO", "OS", "ARCH");
    for(iter_pkg_node = all_pkgs_tree_first(&all_pkgs); 
        iter_pkg_node; 
        iter_pkg_node = all_pkgs_tree_next(&all_pkgs, iter_pkg_node)) {
        //printf("Npkgs %d\n", iter_pkg_node->npkg);

        struct pkg *iter_pkg = NULL;

        list_for_each_entry(iter_pkg, &iter_pkg_node->pkgs_list, all_pkgs_node) {
            if (!strcasestr(iter_pkg->name, filter_name)) {
                if (!strstr(filter_name, "ALL")) {
                    continue;
                }
            }
            print_npkgs++;
            printf("%s%-32s %-32s %-16s %-12s %12s \033[m\n",
                colors[idx_color],
                iter_pkg_node->name,
                iter_pkg->evr, 
                good_repo_name(iter_pkg->repo->name),
                os2string[iter_pkg->repo->os],
                arch2string[iter_pkg->arch]);
        }
        idx_color++;
        if (idx_color >= ncolors)
            idx_color = 0;
    }
    printf("Total print %d packages\n", print_npkgs);
}

static struct pkgs_node* pkgs_find_pkg(char *name)
{
    struct pkgs_node pkg = {
        .name = name,
    };

    return all_pkgs_tree_search(&all_pkgs, &pkg);
}

/**
 *  Format like [name.arch  version  repo]
 *  CUnit.i686   2.1.3-17.el8  centos8_4_appstream
 *  GConf2.i686  3.2.6-22.el8  centos8_4_appstream
 *
 *  Can get the format with command:
 *      dnf --repo=$repo --repofrompath $repo, $url list
 *      $repo: custom name of repo
 *      $url: URL of repodata
 */
int parse_repolist_file(char *filename, os_type os, arch_type repoarch)
{
    int nline = 0;
    char pkgname[128];
    char evr[128];
    char repo_name[128];

    FILE *fp = fopen(filename, "r");
    if (!fp) {
        printf("fopen failed %s\n", filename);
        return -1;
    }
    /* 遍历数据包 */
    while(EOF != fscanf(fp, "%s %s %s\n", pkgname, evr, repo_name)) {

        arch_type arch;
        nline++;

        //printf("%s %s <%s>\n", pkgname, evr, repo_name);
        if (strlen(pkgname) < 2 || strlen(evr) < 2 || strlen(repo_name) < 1) {
            printf("Line %d parse fatal. \n", nline);
            printf("%s %s <%s>\n", pkgname, evr, repo_name);
            exit(1);
        }
        if (strcmp("Added", pkgname) == 0) {
            printf("Prefix line with Added, fatal.\n");
            exit(1);
        }
        
        if (strcasestr(pkgname, NAME_noarch))
            arch = ARCH_noarch;
        else if (strcasestr(pkgname, NAME_i686))
            arch = ARCH_i686;
        else if (strcasestr(pkgname, NAME_x86_64))
            arch = ARCH_x86_64;
        else if (strcasestr(pkgname, NAME_aarch64))
            arch = ARCH_aarch64;
        else if (strcasestr(pkgname, NAME_ppc64le))
            arch = ARCH_ppc64le;
        else if (strcasestr(pkgname, NAME_s390x))
            arch = ARCH_s390x;
        else {
            printf("unsupport arch %s. Press any key to continue.\n", pkgname);
            char ch = getc(stdin);
            continue;
        }
        /**
         *  rpm -qpi xxx.src.rpm 查出来的 Architecture 仍旧有架构信息
         *  这里强制改成 noarch，并删除包名中的架构信息
         */
        if (strcasestr(filename, "source")) {
            int i = strlen(pkgname);
            arch = ARCH_noarch;
            while (pkgname[i--] != '.');
            pkgname[i+1] = '\0';
        }

        struct repo *repo = NULL;
        struct repo *iter_repo = NULL;
        struct pkg *pkg = NULL;

        /* 查找repo是否存在 */
        list_for_each_entry(iter_repo, &repolist, list) {
            //printf("For each repo <%s>\n", iter_repo->name);
            if (strcmp(iter_repo->name, repo_name) == 0) {
                repo = iter_repo;
                break;
            }
        }
        /* 初始化新的repo */
        if (!repo) {
            //printf("New repo <%s>\n", repo_name);
            repo = malloc(sizeof(struct repo));
            memset(repo, 0x00, sizeof(struct repo));
            repo->name = strdup(repo_name);
            repo->npkg = 0;
            repo->os = os;
            repo->arch = repoarch;
            repo_pkg_tree_new(&repo->pkgs);
            list_insert(&repolist, &repo->list);
        }

        /* 添加pkg到repo中 */
        pkg = malloc(sizeof(struct pkg));
        pkg->name = strdup(pkgname);
        pkg->evr = strdup(evr);
        pkg->repo = repo;
        pkg->arch = arch;
        repo_pkg_tree_insert(&repo->pkgs, pkg);
        repo->npkg++;
        repo->arch_npkgs[arch]++;
        
        /* 查找这个包在 全局数中是否存在 */
        struct pkgs_node *pkgs_node = pkgs_find_pkg(pkg->name);
        if (!pkgs_node) {
            pkgs_node = malloc(sizeof(struct pkgs_node));
            pkgs_node->name = strdup(pkg->name);
            pkgs_node->npkg = 0;
            list_init(&pkgs_node->pkgs_list);
            all_pkgs_tree_insert(&all_pkgs, pkgs_node);
        }
        pkgs_node->npkg++;
        list_insert(&pkgs_node->pkgs_list, &pkg->all_pkgs_node);
        repo = NULL;
    }
    
    fclose(fp);
}

static void usage(int argc, char *argv[])
{
    printf("Usage: %s [filetername]\n", argv[0]);
    
    printf("  $ %s kernel       filter `kernel` packages\n", argv[0]);
    printf("  $ %s ALL          show all packages\n", argv[0]);
    printf("\n");
    printf("  Repo name with `.src` subfix means Source RPM, like xxx.src.rpm.\n");
}

int main(int argc, char *argv[])
{
    char *filter_name = NULL;

    filter_name = argv[1];

    vars_init();
    load_metadata();

    for_each_repo(&repo_print_operation);
    if (argc == 1) {
        usage(argc, argv);
    }


    pkgs_for_each_pkg(filter_name);
}